/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.linkis.cli.core.interactor.command.template;

import org.apache.linkis.cli.common.entity.command.CmdOption;
import org.apache.linkis.cli.common.entity.command.CmdTemplate;
import org.apache.linkis.cli.common.entity.command.CmdType;
import org.apache.linkis.cli.common.exception.LinkisClientRuntimeException;
import org.apache.linkis.cli.common.exception.error.ErrorLevel;
import org.apache.linkis.cli.core.constants.CommonConstants;
import org.apache.linkis.cli.core.exception.CommandException;
import org.apache.linkis.cli.core.exception.error.CommonErrMsg;
import org.apache.linkis.cli.core.interactor.command.SpecialMap;
import org.apache.linkis.cli.core.interactor.command.template.option.*;
import org.apache.linkis.cli.core.utils.converter.AbstractStringConverter;
import org.apache.linkis.cli.core.utils.converter.PredefinedStringConverters;

import java.lang.reflect.Field;
import java.util.*;

/**
 * CmdTemplate defines what CmdOption/Flag/Parameter that a command should contains. StdOption:
 * CmdOption.name should starts with '-' String that follows is treated as CmdOption.value. User
 * should input both name and value e.g. --cmd "whoami". Flag: a special type of CmdOption. Only
 * boolean value allowed Parameter: User only input Parameter.value
 */
public abstract class AbstractCmdTemplate implements CmdTemplate, Cloneable {
  /** members */
  protected CmdType cmdType;

  protected List<CmdOption<?>> options;

  /** option name -> CmdOption/Flag/Parameter */
  protected Map<String, CmdOption<?>> optionsMap;

  public AbstractCmdTemplate(final CmdType cmdType) {
    this.cmdType = cmdType;
    options = new ArrayList<>();
    optionsMap = new HashMap<>();
  }

  /** For parameters */
  protected final Parameter<String> parameter(
      String keyPrefix,
      String key,
      String paramName,
      String description,
      boolean isOptional,
      String defaultValue) {
    return parameter(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        PredefinedStringConverters.NO_CONVERTER,
        defaultValue);
  }

  protected final Parameter<Integer> parameter(
      String keyPrefix,
      String key,
      String paramName,
      String description,
      boolean isOptional,
      Integer defaultValue) {
    return parameter(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        PredefinedStringConverters.INT_CONVERTER,
        defaultValue);
  }

  protected final Parameter<String[]> parameter(
      String keyPrefix,
      String key,
      String paramName,
      String description,
      boolean isOptional,
      String[] defaultValue) {
    return parameter(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        PredefinedStringConverters.STR_ARRAY_CONVERTER,
        defaultValue);
  }

  protected final <T> Parameter<T> parameter(
      String keyPrefix,
      String key,
      String paramName,
      String description,
      boolean isOptional,
      AbstractStringConverter<T> converter,
      T defaultValue) {
    Parameter<T> parameter =
        new Parameter<>(
            keyPrefix, key, paramName, description, isOptional, converter, defaultValue);
    options.add(parameter);
    return parameter;
  }

  /** For flags */
  protected final Flag flag(
      String keyPrefix, String key, String[] paramName, String description, boolean defaultValue) {
    return flag(keyPrefix, key, paramName, description, false, defaultValue);
  }

  protected final Flag flag(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      boolean defaultValue) {
    checkIllegalOption(paramName);
    Flag flag = new Flag(keyPrefix, key, paramName, description, isOptional, defaultValue);
    putOption(paramName, flag);
    return flag;
  }

  /** For options */
  protected final StdOption<String> option(
      String keyPrefix, String key, String[] paramName, String description, boolean isOptional) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        null,
        PredefinedStringConverters.NO_CONVERTER);
  }

  protected final StdOption<String> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      String defaultValue) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        defaultValue,
        PredefinedStringConverters.NO_CONVERTER);
  }

  protected final StdOption<String[]> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      String[] defaultValue) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        defaultValue,
        PredefinedStringConverters.STR_ARRAY_CONVERTER);
  }

  protected final StdOption<Integer> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      Integer defaultValue) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        defaultValue,
        PredefinedStringConverters.INT_CONVERTER);
  }

  protected final StdOption<Boolean> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      Boolean defaultValue) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        defaultValue,
        PredefinedStringConverters.BOOLEAN_CONVERTER);
  }

  protected final StdOption<Map<String, String>> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      Map<String, String> defaultValue) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        defaultValue,
        PredefinedStringConverters.STRING_MAP_CONVERTER);
  }

  protected final StdOption<SpecialMap<String, String>> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      SpecialMap<String, String> defaultValue) {
    return option(
        keyPrefix,
        key,
        paramName,
        description,
        isOptional,
        defaultValue,
        PredefinedStringConverters.STRING_SPECIAL_MAP_CONVERTER);
  }

  protected final <T> StdOption<T> option(
      String keyPrefix,
      String key,
      String[] paramName,
      String description,
      boolean isOptional,
      T defaultValue,
      AbstractStringConverter<T> converter) {
    checkIllegalOption(paramName);
    StdOption<T> stdOption =
        new StdOption<>(
            keyPrefix, key, paramName, description, isOptional, defaultValue, converter);
    putOption(paramName, stdOption);
    return stdOption;
  }

  protected final MapOption mapOption(
      String keyPrefix, String key, String[] paramName, String description, boolean isOptional) {
    checkIllegalOption(paramName);
    MapOption option = new MapOption(keyPrefix, key, paramName, description, isOptional);
    putOption(paramName, option);
    return option;
  }

  protected final SpecialMapOption speciaMapOption(
      String keyPrefix, String key, String[] paramName, String description, boolean isOptional) {
    checkIllegalOption(paramName);
    SpecialMapOption option =
        new SpecialMapOption(keyPrefix, key, paramName, description, isOptional);
    putOption(paramName, option);
    return option;
  }

  private void checkIllegalOption(final String[] names) {
    if (names == null || names.length <= 0) {
      throw new IllegalArgumentException("At least one cmdType should be given to CmdOption.");
    } else if (names.length > CommonConstants.MAX_NUM_OF_COMMAND_ARGUEMENTS) {
      throw new IllegalArgumentException(
          "At most "
              + CommonConstants.MAX_NUM_OF_COMMAND_ARGUEMENTS
              + " cmdType can be given to CmdOption.");
    } else {
      for (String name : names) {
        if (!name.startsWith("-")) {
          throw new IllegalArgumentException(name + " should starts with '-'.");
        }
      }
    }
  }

  private void putOption(final String[] names, CmdOption<?> option) {
    options.add(option);
    for (String name : names) {
      optionsMap.put(name, option);
    }
  }

  public CmdType getCmdType() {
    return this.cmdType;
  }

  @Override
  public List<CmdOption<?>> getOptions() {
    return this.options;
  }

  @Override
  public Map<String, CmdOption<?>> getOptionsMap() {
    return this.optionsMap;
  }

  public abstract void checkParams() throws LinkisClientRuntimeException;

  @Override
  protected Object clone() throws CloneNotSupportedException {
    AbstractCmdTemplate ret = (AbstractCmdTemplate) super.clone();

    /*
     for recording field -> paraName relation
    */
    Map<String, List<String>> tmpMap = new HashMap<>();
    for (Map.Entry<String, CmdOption<?>> entry : optionsMap.entrySet()) {
      String key = entry.getValue().getKey();
      if (!tmpMap.containsKey(key)) {
        tmpMap.put(key, new ArrayList<>());
      }
      tmpMap.get(key).add(entry.getKey());
    }

    ret.options = new ArrayList<>();
    ret.optionsMap = new HashMap<>();

    /*
     scan all filds (fields should contains all options/parameters/flags)
    */
    Class clazz = ret.getClass();
    List<Field> fields = new ArrayList<>();
    while (clazz != null) {
      fields.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
      clazz = clazz.getSuperclass();
    }
    for (Field field : fields) {
      Object fieldObj;
      field.setAccessible(true);
      try {
        fieldObj = field.get(ret);
      } catch (IllegalArgumentException | IllegalAccessException e) {
        throw new CommandException(
            "CMD0018",
            ErrorLevel.ERROR,
            CommonErrMsg.ParserParseErr,
            "failed to make deep copy of template: " + this.getCmdType(),
            e);
      }
      if (fieldObj instanceof StdOption<?>
          || fieldObj instanceof MapOption
          || fieldObj instanceof SpecialMapOption) {
        CmdOption<?> opt = ((CmdOption<?>) fieldObj).clone();
        try {
          field.set(ret, opt);
        } catch (Exception e) {
          throw new CommandException(
              "CMD0018",
              ErrorLevel.ERROR,
              CommonErrMsg.ParserParseErr,
              "failed to make deep copy of template: " + this.getCmdType(),
              e);
        }
        ret.options.add(opt);
        List<String> paraNames = tmpMap.get(opt.getKey());
        /*
         reconstruct optionsMap
        */
        for (String paraName : paraNames) {
          ret.optionsMap.put(paraName, opt);
        }
      } else if (fieldObj instanceof Parameter<?>) {
        Parameter<?> param = ((Parameter<?>) fieldObj).clone();
        try {
          field.set(ret, param);
        } catch (Exception e) {
          throw new CommandException(
              "CMD0018",
              ErrorLevel.ERROR,
              CommonErrMsg.ParserParseErr,
              "failed to make deep copy of template: " + this.getCmdType(),
              e);
        }
        ret.options.add(param);
      } else {
        // ignore
      }
    }
    return ret;
  }

  @Override
  public AbstractCmdTemplate getCopy() {
    AbstractCmdTemplate ret;
    try {
      ret = (AbstractCmdTemplate) this.clone();
    } catch (CloneNotSupportedException e) {
      throw new CommandException(
          "CMD0018",
          ErrorLevel.ERROR,
          CommonErrMsg.ParserParseErr,
          "failed to make deep copy of template: " + this.getCmdType(),
          e);
    }

    return ret;
  }
}
